一、C 语言在 Android 中的不可替代性 尽管 Android NDK 开发越来越多地使用 C++,C 语言仍然是系统编程的基石:
Linux 内核全部用 C 编写 。Android 基于 Linux 内核,内核模块和驱动都是 C。
Android 的 Bionic libc 是用 C 实现的。所有系统调用(open、read、write、mmap、ioctl 等)的封装都是 C。
性能关键路径 。ART 运行时的垃圾回收(GC)、JIT 编译器的代码生成、HAL 层的硬件通信,都是 C/汇编。
ABI 稳定性 。C 的 ABI 比 C++ 稳定得多。JNI 的接口是 C 接口(JNIEnv 是 C 函数指针表)。跨语言 FFI(Foreign Function Interface)几乎都基于 C ABI。
二进制体积 。C 运行时极简(Android 的 libc.so 约 500KB),而 C++ 的 STL 会增加显著体积。
AOSP 源码中 bionic/ 目录(Android 的 C 库)和 system/core/ 中的大量工具都用 C 编写。
二、指针深度剖析 指针是 C 语言最具威力也最容易被误用的特性。理解指针是掌握 C 语言进阶的关键。
2.1 指针的底层模型 指针本质上是一个存储内存地址的变量。在 32 位系统上占 4 字节,64 位系统上占 8 字节。但指针不仅仅是地址——它还带有类型信息 ,告诉编译器如何解释指向的内存。
int x = 0x12345678 ;int *p = &x; char *cp = (char *)&x;
2.2 void* 通用指针 void* 是 C 语言中的”万能指针”,可以指向任何类型,但不能直接解引用——必须先转换为具体类型指针。这正是 malloc 返回 void* 的原因:分配的内存可以用于任何类型。
#include <stdlib.h> #include <string.h> void swap (void *a, void *b, size_t size) { void *tmp = malloc (size); if (tmp == NULL ) return ; memcpy (tmp, a, size); memcpy (a, b, size); memcpy (b, tmp, size); free (tmp); } int a = 10 , b = 20 ;swap(&a, &b, sizeof (int )); double x = 1.5 , y = 3.7 ;swap(&x, &y, sizeof (double ));
void* 的一个关键注意事项:对 void* 的指针运算在标准 C 中是未定义行为(GCC 作为扩展允许,将其视为 char* 运算)。正确做法是先转换为 char*:
void *ptr = malloc (100 );void *next = ptr + 10 ; void *ptr = malloc (100 );void *next = (char *)ptr + 10 ;
2.3 函数指针与回调 函数指针是 C 语言实现多态和回调的基础机制:
#include <stdio.h> typedef int (*compare_func_t ) (const void *, const void *) ;void sort (void *base, size_t nmemb, size_t size, compare_func_t cmp) { } int compare_int (const void *a, const void *b) { return (*(int *)a - *(int *)b); } int compare_string (const void *a, const void *b) { return strcmp (*(const char **)a, *(const char **)b); } int main () { int arr[] = {5 , 2 , 8 , 1 , 3 }; sort(arr, 5 , sizeof (int ), compare_int); char *strs[] = {"banana" , "apple" , "cherry" }; sort(strs, 3 , sizeof (char *), compare_string); return 0 ; }
在 Android 的 JNI 环境中,函数指针常用于 native 层的回调注册:
typedef void (*on_data_received_cb) (const uint8_t *data, size_t len, void *user_data) ;typedef struct { on_data_received_cb callback; void *user_data; } DataListener; void register_listener (DataListener *listener, on_data_received_cb cb, void *user_data) { listener->callback = cb; listener->user_data = user_data; } void on_packet_arrived (DataListener *listener, const uint8_t *packet, size_t len) { if (listener->callback) { listener->callback(packet, len, listener->user_data); } }
2.4 函数指针数组——状态机 typedef enum { STATE_IDLE, STATE_CONNECTING, STATE_CONNECTED, STATE_CLOSING, } ConnectionState; typedef void (*state_handler_t ) (void *context) ;static state_handler_t state_handlers[] = { [STATE_IDLE] = handle_idle, [STATE_CONNECTING] = handle_connecting, [STATE_CONNECTED] = handle_connected, [STATE_CLOSING] = handle_closing, }; void dispatch_state (ConnectionState state, void *context) { if (state < sizeof (state_handlers) / sizeof (state_handlers[0 ])) { state_handlers[state](context); } }
2.5 指针运算与数组 int arr[10 ];int *p = arr; int (*pa)[10 ] = &arr; int *p1 = &arr[5 ];int *p2 = &arr[2 ];ptrdiff_t diff = p1 - p2; if (p1 > p2) { }int matrix[3 ][4 ];int (*row_ptr)[4 ] = matrix;
三、内存布局与四区模型 理解 C 语言的内存布局是调试 native crash 和内存相关 bug 的基础。Android 进程的内存空间(虚拟地址空间)布局如下:
高地址 ┌──────────────────┐ │ Kernel Space │ (3GB-4GB in 32-bit, vast in 64-bit) ├──────────────────┤ │ Stack (↓下增) │ 局部变量、函数返回地址 │ ... │ │ ↓ │ │ │ │ ↑ │ │ ... │ │ Heap (↑上增) │ malloc / free ├──────────────────┤ │ BSS (未初始化) │ 未初始化/零初始化的全局变量和静态变量 ├──────────────────┤ │ Data (已初始化) │ 已初始化的全局变量和静态变量 ├──────────────────┤ │ Text (代码段) │ 可执行指令(只读) └──────────────────┘ 低地址
四区模型 :
区域
存储内容
生命周期
管理方式
栈 (Stack)
局部变量、函数参数、返回地址
函数返回时自动销毁
编译器自动管理
堆 (Heap)
malloc/calloc/realloc 分配的内存
手动 free 或进程退出
程序员显式管理
静态区 (Data/BSS)
全局变量、static 变量
程序整个运行期
编译器分配
代码区 (Text)
可执行指令、只读数据(字符串常量)
程序整个运行期
只读
int global_initialized = 42 ; int global_uninitialized; static int static_var = 100 ; const char *msg = "Hello" ; void demo () { int local = 10 ; static int counter = 0 ; counter++; char *heap_mem = (char *)malloc (1024 ); free (heap_mem); }
在 Android NDK 开发中,常见的崩溃类型与内存布局直接相关:
Stack Overflow :递归太深或局部数组过大(Android 默认线程栈大小约 1MB)。
Heap Corruption :double free、use-after-free、buffer overflow。
Segmentation Fault(SIGSEGV) :访问 NULL 指针、访问已释放的内存、写只读内存。
3.1 结构体对齐与填充 结构体在内存中的布局不是简单的字段相加,编译器会插入填充字节(padding)以满足对齐要求:
#include <stddef.h> struct BadLayout { char a; int b; char c; }; struct GoodLayout { int b; char a; char c; }; _Static_assert (sizeof (struct GoodLayout) == 8 , "Unexpected padding" );printf ("offset of a: %zu\n" , offsetof(struct BadLayout, a)); printf ("offset of b: %zu\n" , offsetof(struct BadLayout, b)); printf ("offset of c: %zu\n" , offsetof(struct BadLayout, c));
Android NDK 中的实际影响:当 native 代码与 Java 层通过 JNI 传递结构体时,如果结构体布局不一致(如 native 和 Java 解析的偏移不同),会导致数据错乱。使用 __attribute__((packed)) 可以禁用对齐填充,但会降低内存访问性能(非对齐访问在某些 ARM 架构上会触发 SIGBUS)。
3.2 联合体(union) 联合体的所有成员共享同一块内存区域,大小为最大成员的大小。在 Android 底层开发中,联合体常用于协议解析和类型双关:
union IPv4 { uint32_t addr; uint8_t octets[4 ]; struct { uint8_t a, b, c, d; }; }; union IPv4 ip ;ip.addr = 0x0A000001 ; printf ("%d.%d.%d.%d\n" , ip.octets[3 ], ip.octets[2 ], ip.octets[1 ], ip.octets[0 ]); union FloatInspector { float f; uint32_t bits; struct { uint32_t mantissa : 23 ; uint32_t exponent : 8 ; uint32_t sign : 1 ; }; };
四、位运算与位域 C 语言的位运算是嵌入式开发和协议解析的利器。Android 的 Binder 协议、HAL 接口标志位、编解码器的比特流操作都大量使用位运算。
#define FLAG_A (1 << 0) #define FLAG_B (1 << 1) #define FLAG_C (1 << 2) uint32_t flags = 0 ;flags |= FLAG_A; flags |= (FLAG_B | FLAG_C); flags &= ~FLAG_A; if (flags & FLAG_B) { } flags ^= FLAG_C; uint8_t extracted = (flags >> 4 ) & 0x0F ;size_t aligned = (size + 3 ) & ~3 ;int is_power_of_two (unsigned int x) { return x && !(x & (x - 1 )); } int popcount (unsigned int x) { int count = 0 ; while (x) { x &= (x - 1 ); count++; } return count; }
位域(Bit Field)可以更自然地表达硬件寄存器和协议头:
struct ip_header { uint8_t version_ihl; uint8_t dscp_ecn; uint16_t total_length; uint16_t identification; uint16_t flags_fragment; uint8_t ttl; uint8_t protocol; uint16_t header_checksum; uint32_t source_ip; uint32_t dest_ip; }; struct ip_header_bitfield { unsigned int ihl : 4 ; unsigned int version : 4 ; unsigned int ecn : 2 ; unsigned int dscp : 6 ; }; #define IP_VERSION(hdr) (((hdr)->version_ihl & 0xF0) >> 4) #define IP_IHL(hdr) ((hdr)->version_ihl & 0x0F)
位域的可移植性注意事项:位域的内存布局(从 LSB 还是 MSB 开始分配)是编译器实现定义的,跨平台通信时不应该在位域结构体上直接做 memcpy——应该手动编码/解码到字节序列。
五、C 预处理器宏 宏是 C 语言元编程的手段。在 Android 底层代码中非常常见:
#define MAKE_FUNC(name) native_##name #define STRINGIFY(x) #x #define TO_STRING(x) STRINGIFY(x) #define SAFE_FREE(p) do { \ if ((p) != NULL) { \ free(p); \ (p) = NULL; \ } \ } while (0) #ifdef DEBUG #define LOG_TAG "NativeLib" #include <android/log.h> #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #else #define LOGD(...) ((void)0) #define LOGW(...) ((void)0) #define LOGE(...) ((void)0) #endif #define STATIC_ASSERT(cond) _Static_assert(cond, #cond) STATIC_ASSERT(sizeof (int ) == 4 ); #define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member))) struct Node { int value; struct Node *next ; struct Node *prev ; }; void remove_node (struct Node *node) { node->prev->next = node->next; node->next->prev = node->prev; }
5.1 宏的陷阱 #define MAX(a, b) ((a) > (b) ? (a) : (b)) int x = 5 ;int y = MAX(x++, 10 ); #define MAX_SAFE(a, b) ({ \ __auto_type _a = (a); \ __auto_type _b = (b); \ _a > _b ? _a : _b; \ }) #define DOUBLE(x) (x * 2) #define SQUARE(x) (x * x) int result = SQUARE(DOUBLE(3 ));
六、setjmp/longjmp 与异常处理 C 语言没有像 C++ 的 try-catch 异常机制,但 setjmp/longjmp 提供了非本地跳转(non-local goto),可以实现异常安全:
#include <setjmp.h> static jmp_buf g_error_jmp;void process_data (const char *filename) { if (setjmp(g_error_jmp) != 0 ) { printf ("Error occurred during processing, rolling back...\n" ); rollback_transaction(); return ; } open_file(filename); parse_header(); process_body(); } void parse_header () { if (header_corrupted()) { longjmp(g_error_jmp, 1 ); } }
实际中,Android 的 libpng(PNG 解码库)、libjpeg-turbo(JPEG 编解码器)等 C 库内部使用 setjmp/longjmp 实现错误处理。当解码失败时,从深层递归/循环中直接跳回错误处理点,避免了逐层返回和检查返回值的繁琐。
注意:longjmp 不会调用 C++ 对象的析构函数(不会栈展开),所以在 C++ 中使用需要特别小心。在 C 中,使用 setjmp/longjmp 时需要确保不会泄漏已分配的资源(通常在 setjmp 调用处维护一个资源列表用于回滚)。
6.1 setjmp/longjmp 的实现原理 setjmp 保存的上下文通常包括:
程序计数器(PC / IP):当前执行位置
栈指针(SP)
帧指针(FP)
所有 callee-saved 寄存器(在 ARM64 上为 x19-x28)
信号掩码(如果使用 sigsetjmp)
longjmp 恢复这些寄存器值,使执行流”跳回”到 setjmp 处。由于不进行栈展开,longjmp 的性能远优于 C++ 异常——但安全性和正确性完全由开发者保证。
volatile int error_code = 0 ;static jmp_buf jmp;if ((error_code = setjmp(jmp)) != 0 ) { switch (error_code) { case 1 : handle_io_error(); break ; case 2 : handle_parse_error(); break ; case 3 : handle_memory_error(); break ; } return ; } longjmp(jmp, 2 );
七、pthread 线程基础 Android 的 Bionic 完整支持 POSIX 线程(pthread)。虽然 Android NDK 可以使用 C++11 的 std::thread,但 pthread 仍然是系统级线程操作的基础:
#include <pthread.h> #include <android/log.h> typedef struct { int thread_id; const char *name; void *(*start_routine)(void *); void *arg; } ThreadParams; static void *thread_entry (void *arg) { ThreadParams *params = (ThreadParams *)arg; pthread_setname_np(pthread_self(), params->name); __android_log_print(ANDROID_LOG_INFO, "NativeThread" , "Thread %d '%s' started" , params->thread_id, params->name); void *result = params->start_routine(params->arg); __android_log_print(ANDROID_LOG_INFO, "NativeThread" , "Thread %d '%s' finished" , params->thread_id, params->name); return result; } int create_thread (ThreadParams *params) { pthread_t thread; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); int ret = pthread_create(&thread, &attr, thread_entry, params); pthread_attr_destroy(&attr); return ret; } static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;static int ready = 0 ;void producer () { pthread_mutex_lock(&mutex); ready = 1 ; pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); } void consumer () { pthread_mutex_lock(&mutex); while (!ready) { pthread_cond_wait(&cond, &mutex); } pthread_mutex_unlock(&mutex); }
7.1 pthread 同步深入 pthread_spinlock_t spin;pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE); pthread_spin_lock(&spin); pthread_spin_unlock(&spin); pthread_spin_destroy(&spin); pthread_rwlock_t rwlock;pthread_rwlock_init(&rwlock, NULL ); pthread_rwlock_rdlock(&rwlock); pthread_rwlock_unlock(&rwlock); pthread_rwlock_wrlock(&rwlock); pthread_rwlock_unlock(&rwlock); pthread_rwlock_destroy(&rwlock); pthread_barrier_t barrier;pthread_barrier_init(&barrier, NULL , 4 ); pthread_barrier_wait(&barrier); pthread_barrier_destroy(&barrier);
八、常见安全漏洞与防护 8.1 缓冲区溢出(Buffer Overflow) 经典的 C 安全漏洞。Android 的 Stagefright 漏洞系列中多个都是基于缓冲区溢出。
void unsafe_copy (char *input) { char buffer[64 ]; strcpy (buffer, input); } void safe_copy (char *input) { char buffer[64 ]; strncpy (buffer, input, sizeof (buffer) - 1 ); buffer[sizeof (buffer) - 1 ] = '\0' ; } snprintf (buffer, sizeof (buffer), "%s" , input);
8.2 Use-After-Free(释放后使用) struct Connection *conn = create_connection();process(conn); free (conn);conn->send_data(); free (conn);conn = NULL ; if (conn != NULL ) { conn->send_data(); }
8.3 格式字符串漏洞 void log_user_input (char *user_input) { printf (user_input); } void log_user_input (char *user_input) { printf ("%s" , user_input); }
8.4 整数溢出 void allocate_buffer (size_t count, size_t elem_size) { size_t total = count * elem_size; char *buf = malloc (total); } void allocate_buffer (size_t count, size_t elem_size) { if (count > 0 && elem_size > SIZE_MAX / count) { return ; } size_t total = count * elem_size; char *buf = malloc (total); }
九、C11/C17 新特性 Android NDK 支持 GCC 和 Clang,后者对 C 标准的支持更完备。以下是一些值得使用的现代 C 特性:
#include <stdatomic.h> atomic_int counter = ATOMIC_VAR_INIT(0 );int old = atomic_fetch_add(&counter, 1 ); int val = atomic_load (&counter); #include <threads.h> thrd_t thread;thrd_create(&thread, thread_func, NULL ); thrd_join(thread, NULL ); struct Person { char *name; union { int age; int birth_year; }; }; void *aligned_ptr = aligned_alloc(64 , 1024 ); free (aligned_ptr);
十、面试常问题目 Q1: 栈(Stack)和堆(Heap)的区别?在 Android NDK 中如何选择?
栈由编译器自动管理,分配极快(移动栈指针),函数返回时自动回收,但空间有限(Android 中每个线程约 1MB 默认栈大小)。堆由 malloc/free 手动管理,分配较慢(需要查找空闲块),空间大(受限于进程虚拟地址空间),但容易内存泄漏和碎片化。规则:小的固定大小对象放栈上,大的动态大小对象放堆上。Android NDK 中大型数组(如解码后的图片缓冲区)必须用堆分配。注意 Android 可以通过 pthread_attr_setstacksize() 自定义栈大小,但增大栈会减少堆的可用地址空间(在 32 位系统上尤其明显)。
Q2: 函数指针和回调的典型应用场景?
函数指针在 C 中用于实现策略模式和多态:(1) 排序/查找算法的比较函数(qsort、bsearch);(2) 事件驱动的回调注册(JNI 回调 Java 方法本质就是函数指针+JNI 环境);(3) 状态机的状态转移表;(4) 插件系统的接口抽象(动态库加载通过 dlsym 获取函数指针);(5) 信号处理函数(signal handler)。
Q3: C 的宏和 C++ 的模板有什么区别?
宏是预处理器文本替换,没有类型检查,容易产生边界效应(需要用括号保护参数和整体),调试困难(编译错误指向展开后的代码)。模板是编译器层面的代码生成,有类型检查,生成的代码经过了完整的语义分析,错误信息更有意义。宏可以生成任意的代码文本(包括控制流和声明),模板主要适用于泛型算法和类型安全的容器。在 C 语言中宏是唯一的代码生成手段,在 C++ 中优先使用模板。
Q4: Android Bionic libc 和 glibc 有什么区别?
Bionic 是 Android 定制的 C 库(源码路径:bionic/libc/),专为移动设备优化。主要区别:(1) Bionic 体积更小(~500KB vs glibc 的几 MB);(2) Bionic 没有完整的 locale 支持;(3) Bionic 的 pthread 实现简化(基于 futex 而非 NPTL);(4) Bionic 对一些 POSIX 函数没有实现或有限制(如 system() 在某些 Android 版本被限制);(5) Bionic 的 DNS 解析集成到 netd 守护进程。这些区别使得一些 Linux 程序移植到 Android 时需要额外适配。
Q5: 结构体中的 padding 是什么?如何避免不必要的 padding?如何强制紧凑布局?
Padding 是编译器为了满足硬件对齐要求在结构体成员之间或末尾插入的填充字节。减少 padding 的策略:(1) 按照对齐要求从大到小排列成员(double/long long 在前,char/short 在后);(2) 将相同宽度的成员聚合在一起;(3) 如果需要强制紧凑布局(如与硬件寄存器或网络协议匹配),使用 __attribute__((packed)) 或 #pragma pack(1),但要注意这会导致非对齐访问——在某些 ARM 架构(如 ARMv5)上非对齐访问会引发异常,在 ARMv7+ 上虽然硬件支持但性能降低。
参考源码路径:
Bionic libc:bionic/libc/
Bionic pthread:bionic/libc/bionic/pthread_create.cpp
Bionic malloc (jemalloc/scudo):bionic/libc/bionic/jemalloc_wrapper.cpp
Linux 内核内存管理:kernel/msm-*/mm/
AOSP 原生工具:system/core/toolbox/
Binder 协议定义:frameworks/native/libs/binder/